#include "network_interface.hh"

#include "arp_message.hh"
#include "ethernet_frame.hh"

#include <iostream>
// Dummy implementation of a network interface
// Translates from {IP datagram, next hop address} to link-layer frame, and from link-layer frame to IP datagram

// For Lab 5, please replace with a real implementation that passes the
// automated checks run by `make check_lab5`.

// You will need to add private members to the class declaration in `network_interface.hh`

template <typename... Targs>
void DUMMY_CODE(Targs &&.../* unused */) {}

using namespace std;

//! \param[in] ethernet_address Ethernet (what ARP calls "hardware") address of the interface
//! \param[in] ip_address IP (what ARP calls "protocol") address of the interface
NetworkInterface::NetworkInterface(const EthernetAddress &ethernet_address, const Address &ip_address)
    : _ethernet_address(ethernet_address), _ip_address(ip_address) {
    cerr << "DEBUG: Network interface has Ethernet address " << to_string(_ethernet_address) << " and IP address "
         << ip_address.ip() << "\n";
}

//! \param[in] dgram the IPv4 datagram to be sent
//! \param[in] next_hop the IP address of the interface to send it to (typically a router or default gateway, but may also be another host if directly connected to the same network as the destination)
//! (Note: the Address type can be converted to a uint32_t (raw 32-bit IP address) with the Address::ipv4_numeric() method.)
void NetworkInterface::send_datagram(const InternetDatagram &dgram, const Address &next_hop) {
    // convert IP address of next hop to raw 32-bit representation (used in ARP header)
    const uint32_t next_hop_ip = next_hop.ipv4_numeric();

    // construct the frame
    EthernetFrame send;
    send.header().src = _ethernet_address;
    send.header().type = EthernetHeader::TYPE_IPv4;
    send.payload() = BufferList(dgram.serialize());

    auto arpmap_it = _arpmap.find(next_hop_ip);
    // std::cerr << __LINE__ << ":"
    //         << Address::from_ipv4_numeric(next_hop_ip).to_string()
    //         << " is" << (arpmap_it != _arpmap.end() ? "":" not") << " in ARP cache"
    //         << std::endl;
    if (arpmap_it != _arpmap.end()) {
        // hit arp cache
        // std::cerr << __LINE__ << ":" << "is" << (arpmap_it->second.valid ? "":" not") << " in" << std::endl;
        if (arpmap_it->second.valid) {
            // entry is valid
            send.header().dst = arpmap_it->second.mac;
            _frames_out.push(send);
        } else {
            // not valid , put frame into flight
            _waiting_for_send.emplace_back(next_hop_ip, send);
            // std::cerr << __LINE__ << ":" << "put in flight" << std::endl;
        }
    } else {
        // put into flight
        _waiting_for_send.emplace_back(next_hop_ip, send);

        // add entry in arp cache
        ip2mac inv_entry(_timer);
        _arpmap.insert({next_hop_ip, inv_entry});

        // ask for MAC for the next hop
        ARPMessage arp_request;

        arp_request.opcode = ARPMessage::OPCODE_REQUEST;

        arp_request.sender_ip_address = _ip_address.ipv4_numeric();
        arp_request.sender_ethernet_address = _ethernet_address;

        arp_request.target_ip_address = next_hop_ip;
        arp_request.target_ethernet_address = ETHERNET_LOOKING;

        EthernetFrame ef;

        ef.header().src = _ethernet_address;
        ef.header().dst = ETHERNET_BROADCAST;
        ef.header().type = EthernetHeader::TYPE_ARP;

        ef.payload() = BufferList(arp_request.serialize());
        // std::cerr << __LINE__ << ":" << "A Request frame " << arp_request.to_string() << " is sent" << std::endl;
        _frames_out.push(ef);
    }
}

//! \param[in] frame the incoming Ethernet frame
optional<InternetDatagram> NetworkInterface::recv_frame(const EthernetFrame &frame) {
    if (frame.header().dst != _ethernet_address && frame.header().dst != ETHERNET_BROADCAST)
        return {};

    if (frame.header().type == EthernetHeader::TYPE_IPv4 && frame.header().dst == _ethernet_address) {
        // if is IPv4

        // parse it
        InternetDatagram rec;
        ParseResult ps = rec.parse(frame.payload().concatenate());
        if (ps != ParseResult::NoError)
            return {};

        return rec;
    } else if (frame.header().type == EthernetHeader::TYPE_ARP) {
        // if is ARP
        // sstd::cerr <<  __LINE__ << ":" "ARP Frame:" <<  frame.header().to_string() << " comes" << std::endl;

        // parse to ARP
        ARPMessage arp_packet;
        ParseResult ps = arp_packet.parse(frame.payload().concatenate());
        if (ps != ParseResult::NoError)
            return {};

        if (arp_packet.opcode != ARPMessage::OPCODE_REQUEST && arp_packet.opcode != ARPMessage::OPCODE_REPLY)
            return {};
        else {
            // what ever it it a Request or reply, first update the ARP cache

            //! this means :
            // 1. the invalid entry before now is avaliable
            // 2. new entry
            bool updated = false;
            auto it = _arpmap.find(arp_packet.sender_ip_address);
            // std::cerr << __LINE__ << ":"
            //     << Address::from_ipv4_numeric(arp_packet.sender_ip_address).to_string()
            //     << " is" << (it != _arpmap.end() ? "":" not") << " in ARP cache"
            //     << std::endl;
            if (it != _arpmap.end()) {
                updated = !it->second.valid;
                it->second.valid = true;
                it->second.arrived_time = _timer;
                it->second.mac = arp_packet.sender_ethernet_address;
            } else {
                updated = true;
                ip2mac remote(arp_packet.sender_ethernet_address, _timer);
                _arpmap.insert({arp_packet.sender_ip_address, remote});
            }

            // if it's a request ,try to send a reply first
            if (arp_packet.opcode == ARPMessage::OPCODE_REQUEST) {
                // if a request
                // check whether target is myself
                Address target_ip_address(Address::from_ipv4_numeric(arp_packet.target_ip_address));
                if (target_ip_address == _ip_address) {
                    // reply to remote
                    ARPMessage arp_reply;
                    arp_reply.opcode = ARPMessage::OPCODE_REPLY;

                    arp_reply.sender_ip_address = _ip_address.ipv4_numeric();
                    arp_reply.sender_ethernet_address = _ethernet_address;

                    arp_reply.target_ip_address = arp_packet.sender_ip_address;
                    arp_reply.target_ethernet_address = arp_packet.sender_ethernet_address;

                    EthernetFrame ef;
                    ef.payload() = arp_reply.serialize();

                    ef.header().src = _ethernet_address;
                    ef.header().dst = arp_packet.sender_ethernet_address;
                    ef.header().type = EthernetHeader::TYPE_ARP;

                    _frames_out.push(ef);
                }
            }
            // if cache updated , send the frame on filght
            if (updated) {
                uint32_t sender_ip_numeric = arp_packet.sender_ip_address;
                // find and send
                for (auto &item : _waiting_for_send) {
                    if (item.next_hop == sender_ip_numeric) {
                        item.sent = true;
                        item.frame.header().dst = arp_packet.sender_ethernet_address;
                        _frames_out.push(item.frame);
                    }
                }
                // remove all the sent frame
                auto es_start = std::remove_if(_waiting_for_send.begin(),
                                               _waiting_for_send.end(),
                                               [](const WaitingItem &i) -> bool { return i.sent; });
                _waiting_for_send.erase(es_start, _waiting_for_send.end());
            }
            return {};
        }
    } else {
        // no match situation
        return {};
    }
}

//! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method
void NetworkInterface::tick(const size_t ms_since_last_tick) {
    _timer += ms_since_last_tick;
    for (auto it = _arpmap.begin(); it != _arpmap.end();) {
        auto const [k, v] = *it;
        if (!v.valid && _timer - v.arrived_time > 5000)
            it = _arpmap.erase(it);
        else if (v.valid && _timer - v.arrived_time > 30000)
            it = _arpmap.erase(it);
        else
            ++it;
    }
}
